{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\n",
    "import warnings\n",
    "\n",
    "warnings.simplefilter(action='ignore', category=FutureWarning)\n",
    "\n",
    "# Load the rating and movie files\n",
    "ratings = pd.read_csv(\"https://s3-us-west-2.amazonaws.com/recommender-tutorial/ratings.csv\")\n",
    "movies = pd.read_csv(\"https://s3-us-west-2.amazonaws.com/recommender-tutorial/movies.csv\")\n",
    "\n",
    "n_ratings = len(ratings)\n",
    "n_movies = len(ratings['movieId'].unique())\n",
    "n_users = len(ratings['userId'].unique())\n",
    "\n",
    "print(f\"Number of ratings: {n_ratings}\")\n",
    "print(f\"Number of unique movieId's: {n_movies}\")\n",
    "print(f\"Number of unique users: {n_users}\")\n",
    "print(f\"Average ratings per user: {round(n_ratings / n_users, 2)}\")\n",
    "print(f\"Average ratings per movie: {round(n_ratings / n_movies, 2)}\")\n",
    "\n",
    "user_freq = ratings[['userId', 'movieId']].groupby('userId').count().reset_index()\n",
    "user_freq.columns = ['userId', 'n_ratings']\n",
    "\n",
    "# Find Lowest and Highest rated movies:\n",
    "mean_rating = ratings.groupby('movieId')[['rating']].mean()\n",
    "\n",
    "# Lowest rated movies\n",
    "lowest_rated = mean_rating['rating'].idxmin()\n",
    "print(movies.loc[movies['movieId'] == lowest_rated])\n",
    "\n",
    "# Highest rated movies\n",
    "highest_rated = mean_rating['rating'].idxmax()\n",
    "print(movies.loc[movies['movieId'] == highest_rated])\n",
    "\n",
    "# Show number of people who rated the highest and lowest rated movies\n",
    "print(ratings[ratings['movieId'] == highest_rated])\n",
    "print(ratings[ratings['movieId'] == lowest_rated])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from scipy.sparse import csc_matrix\n",
    "\n",
    "def create_matrix(df):\n",
    "    N = len(df['userId'].unique())\n",
    "    M = len(df['movieId'].unique())\n",
    "\n",
    "    user_mapper = dict(zip(np.unique(df[\"userId\"]), list(range(N))))\n",
    "    movie_mapper = dict(zip(np.unique(df[\"movieId\"]), list(range(M))))\n",
    "\n",
    "    user_inv_mapper = dict(zip(list(range(N)), np.unique(df[\"userId\"])))\n",
    "    movie_inv_mapper = dict(zip(list(range(M)), np.unique(df[\"movieId\"])))\n",
    "\n",
    "    user_index = [user_mapper[i] for i in df['userId']]\n",
    "    movie_index = [movie_mapper[i] for i in df['movieId']]\n",
    "\n",
    "    X = csc_matrix((df[\"rating\"], (movie_index, user_index)), shape=(M, N))\n",
    "\n",
    "    return X, user_mapper, movie_mapper, user_inv_mapper, movie_inv_mapper\n",
    "\n",
    "X, user_mapper, movie_mapper, user_inv_mapper, movie_inv_mapper = create_matrix(ratings)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.neighbors import NearestNeighbors\n",
    "\n",
    "def find_similar_movies(movie_id, X, k, metric='cosine', show_distance=False):\n",
    "    neighbour_ids = []\n",
    "\n",
    "    movie_ind = movie_mapper[movie_id]\n",
    "    movie_vec = X[movie_ind]\n",
    "\n",
    "    k += 1\n",
    "    kNN = NearestNeighbors(n_neighbors=k, algorithm=\"brute\", metric=metric)\n",
    "    kNN.fit(X)\n",
    "    movie_vec = movie_vec.reshape(1, -1)\n",
    "    neighbour = kNN.kneighbors(movie_vec, return_distance=show_distance)\n",
    "    for i in range(0, k):\n",
    "        n = neighbour.item(i)\n",
    "        neighbour_ids.append(movie_inv_mapper[n])\n",
    "    neighbour_ids.pop(0)\n",
    "    return neighbour_ids\n",
    "\n",
    "movie_titles = dict(zip(movies['movieId'], movies['title']))\n",
    "\n",
    "movie_id = 3\n",
    "similar_ids = find_similar_movies(movie_id, X, k=20)\n",
    "movie_title = movie_titles[movie_id]\n",
    "\n",
    "print(f\"Since you watched {movie_title}, you may like the following:\")\n",
    "for i in similar_ids:\n",
    "    print(movie_titles[i])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from scipy.sparse.linalg import svds\n",
    "\n",
    "ut, s, vt = svds(X, k=100)\n",
    "Q_0 = ut\n",
    "P_0 = np.transpose(np.diag(s) @ vt)\n",
    "\n",
    "Y = Q_0 @ np.transpose(P_0)\n",
    "\n",
    "print('user_0 will rate movie_10 to movie_19 as follows:')\n",
    "for movie_idx in range(10, 20):\n",
    "    print(Y[movie_idx, 0])\n",
    "\n",
    "values = Y[:, 0].tolist()\n",
    "top_k_indices = np.argsort(values)[-20:][::-1]\n",
    "print(f\"user {user_inv_mapper[0]}, may like the following movies:\")\n",
    "for i in top_k_indices:\n",
    "    print(movie_inv_mapper[i], movie_titles[movie_inv_mapper[i]])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def matrix_factorization(R, P, Q, K, steps=5000, alpha=0.0002, beta=0.02):\n",
    "    Q = Q.T\n",
    "    for _ in range(steps):\n",
    "        for i in range(len(R)):\n",
    "            for j in range(len(R[i])):\n",
    "                if R[i, j] > 0:\n",
    "                    eij = R[i, j] - np.dot(P[i, :], Q[:, j])\n",
    "                    for k in range(K):\n",
    "                        P[i][k] = P[i][k] + alpha * (2 * eij * Q[k][j] - beta * P[i][k])\n",
    "                        Q[k][j] = Q[k][j] + alpha * (2 * eij * P[i][k] - beta * Q[k][j])\n",
    "        e = 0\n",
    "        for i in range(len(R)):\n",
    "            for j in range(len(R[i])):\n",
    "                if R[i, j] > 0:\n",
    "                    e = e + pow(R[i, j] - np.dot(P[i, :], Q[:, j]), 2)\n",
    "                    for k in range(K):\n",
    "                        e = e + (beta/2) * (pow(P[i][k], 2) + pow(Q[k][j], 2))\n",
    "        if e < 0.001:\n",
    "            break\n",
    "    return P, Q.T\n",
    "\n",
    "R = X.toarray()\n",
    "\n",
    "K = 100\n",
    "\n",
    "P = np.random.rand(R.shape[0], K)\n",
    "Q = np.random.rand(R.shape[1], K)\n",
    "\n",
    "nP, nQ = matrix_factorization(R, P, Q, K)\n",
    "\n",
    "Y = nP @ nQ.T\n",
    "print('user_0 will rate movie_10 to movie_19 as follows (after SGD):')\n",
    "for movie_idx in range(10, 20):\n",
    "    print(Y[movie_idx, 0])\n",
    "\n",
    "values = Y[:, 0].tolist()\n",
    "top_k_indices = np.argsort(values)[-20:][::-1]\n",
    "print(f\"user {user_inv_mapper[0]}, may like the following movies (after SGD):\")\n",
    "for i in top_k_indices:\n",
    "    print(movie_inv_mapper[i], movie_titles[movie_inv_mapper[i]])"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "cisc7021",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.10.15"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
